package com.kiss.fw.utils;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.apache.log4j.Logger;

import com.kiss.fw.context.EmailContext;
import com.kiss.fw.context.NotificationContext;
import com.kiss.fw.exceptions.ValidationException;
import com.kiss.fw.notifications.EmailNotification;

public final class Util {
	// private static members that should only be visible to this class
	private static final Field[] constantExceptionsfields = Constants.Exceptions.class.getFields();
	private static final Logger logger = Logger.getLogger(Util.class);
	private static ResourceBundle applicationProperties = null;
	private static SecureRandom secureRandom = new SecureRandom();
	
	// public static members that can be shared
	public static final String SPECIAL_CHARACTERS = "}{%.,?/|~`!@#$%^&*()_+- ";
	
	/**
	 * Default constructor should be disabled since all methods are static
	 */
	private Util() {
		logger.warn("Default constructor invoked");
	}
	
	/**
	 * Returns a trimmed string.  Specify whether to keep null reference if
	 * the string passed is null.
	 * 
	 * @param String _string
	 * @param boolean _keepNullReference
	 * @return String
	 */
	public static final String trimString(final String _string, final boolean _keepNullReference) {
		return _string == null ? _keepNullReference ? _string : "" : _string.trim();
	}
	
	/**
	 * Returns a trimmed string. If string is null,
	 * an empty string will be returned.
	 * 
	 * @param String _string
	 * @return String
	 */
	public static final String trimString(final String _string) {
		return trimString(_string, false);
	}
	
	/**
	 * Returns whether object passed in is null.
	 * 
	 * @param Object _object
	 * @return boolean
	 */
	public static boolean isObjectNull(final Object _object) {
		return _object == null;
	}
	
	/**
	 * Returns whether string is empty, a string with only spaces will be
	 * considered empty. This call is equivelant to: 
	 * <code>ESSOUtil.isStringEmpty(_string, false)</code>
	 * 
	 * @param String _string
	 * @return boolean
	 */
	public static boolean isStringEmpty(final String _string) {
		return isStringEmpty(_string, false);
	}
	
	/**
	 * @param String _string
	 * @param boolean _ignoreSpaces
	 * @return boolean
	 */
	public static boolean isStringEmpty(final String _string, final boolean _allowSpaces) {
		boolean returnVal = false;
		
		// TODO: Verify who is using this method... implementation has bug, logic should be flipped
		if(_allowSpaces) {
			if(_string == null || _string.length() <= 0) {
				returnVal = true;
			}
		}
		else {
			returnVal = "".equals(trimString(_string));
		}
		
		return returnVal;
	}
	
	/**
	 * Returns the class name without the package.
	 * to get the fully qualified class name use: Class.getName()
	 * 
	 * @param Class _class
	 * @return String
	 */
	public static String getClassNameWithoutPackage(final Class<?> _class) {
		String className = _class.getName();
		int firstClassCharacter = className.lastIndexOf('.') + 1;
		
		if(firstClassCharacter > 0) {
			className = className.substring(firstClassCharacter);
		}
		
		return className;
	}
	
	public static Object getClassInstance(final String _class, ClassLoader _classLoader) throws Exception {
		return Class.forName(_class, true, _classLoader).newInstance();
	}
	
	/**
	 * Returns an List representation of the Object[]
	 * 
	 * @param Object[] _array
	 * @return List<Object>
	 */
	public static List<Object> toList(final Object[] _array) {
		List<Object> returnVal = null;
		
		if(_array != null) {
			returnVal = new ArrayList<Object>(_array.length);
			
			for(Object tempObject : _array) {
				returnVal.add(tempObject);
			}
		}
		
		return returnVal;
	}
	
	public static String getDisplayDate(final Calendar _calendar, final Locale _locale) {
		StringBuffer sb = new StringBuffer();
		sb.append(_calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, _locale));
		sb.append(' ');
		sb.append(_calendar.get(Calendar.DAY_OF_MONTH));
		sb.append(", ");
		sb.append(_calendar.get(Calendar.YEAR));
		
		return sb.toString();
	}
	
	private static void loadProperties() {
		if(applicationProperties == null) {
			try {
				applicationProperties = ResourceBundle.getBundle("resources.application");
			}
			catch(Throwable _exception) {
				logger.error("Error loading properties file", _exception);
			}
		}
	}
	
	public static List<String> getPropertyKeys(final String _baseKey) {
		loadProperties();
		
		Set<String> keys = applicationProperties.keySet();
		Iterator<String> keysIterator = keys.iterator();
		List<String> returnVal = new ArrayList<String>(keys.size());
		
		while(keysIterator.hasNext()) {
			String tempKey = keysIterator.next();
			
			if(tempKey.indexOf(_baseKey, 0) >= 0) {
				returnVal.add(tempKey);
			}
		}
		
		return returnVal;
	}
	
	public static Map<String, String> getProperties(final String _baseKey) {
		loadProperties();
		
		Set<String> keys = applicationProperties.keySet();
		Iterator<String> keysIterator = keys.iterator();
		Map<String, String> returnVal = new HashMap<String, String>();
		
		while(keysIterator.hasNext()) {
			String tempKey = keysIterator.next();
			
			if(tempKey.indexOf(_baseKey, 0) >= 0) {
				returnVal.put(tempKey, getProperty(tempKey));
			}
		}
		
		return returnVal;
	}
	
	/**
	 * Retrieves the value for the specified key, if the
	 * key doesnt exist an empty string will be returned.
	 * 
	 * @param String _key
	 * @return String
	 */
	public static String getProperty(final String _key) {
		loadProperties();
		
		String returnVal = null;
		
		try {
			returnVal = trimString(applicationProperties.getString(_key));
		}
		catch(Exception _exception) {
			returnVal = "";
		}
		
		return returnVal;
	}
	
	/**
	 * Generates an alphanumeric string with the length specified and without
	 * the characters specified.
	 * 
	 * @param int _length
	 * @param char[] _charactersToOmit
	 * @return String
	 */
	public static String generateRandomAlphaNumericString(final int _length, final char[] _charactersToOmit) {
		StringBuffer password = new StringBuffer();
		char tempChar;
		
		while(password.length() < _length) {
			tempChar = (char)(secureRandom.nextInt(126));
			
			if(Character.isLetterOrDigit(tempChar)) {
				boolean addCharacter = true;
				
				for(char omitChar : _charactersToOmit) {
					if(omitChar == tempChar) {
						addCharacter = false;
						break;
					}
				}
				
				if(addCharacter) {
					password.append(tempChar);
				}
			}
		}

		return password.toString();
	}
	
	/**
	 * Sends email.
	 * 
	 * @param String _from
	 * @param String[] _to
	 * @param String _subject
	 * @param String _body
	 * @param List<Object>_attachments
	 * @param String _mailSessionJndi
	 * @return
	 */
	public static boolean sendEmail(EmailContext _context) {
		Session mailSession = (Session)lookup(null, _context.getJndi(), true);
		boolean returnVal = false;
		
		if(mailSession != null) {
			try {
				MimeMessage message = new MimeMessage(mailSession);
				
				if(message != null) {
					message.setSubject(_context.getSubject());
					message.setText(_context.getBody());
					
					// if we need to override the FROM address
					if(_context.getFrom() != null) {
						message.setFrom(_context.getFrom());
					}
					
					// adding recipients
					List<Address> toAddresses = _context.getTo();
					
					for(Address tempTo : toAddresses) {
						message.addRecipient(Message.RecipientType.TO, tempTo);
					}
					
					// sending the email
					Transport.send(message);
					
					// update flag
					returnVal = true;
				}
			}
			catch(Exception _exception) {
				StringBuffer sb = new StringBuffer("sendEmail() - Error sending email");
				
				if(_exception.getMessage() != null) {
					sb.append(": ");
					sb.append(_exception.getMessage());
					logger.warn(sb);
				}
				else {
					logger.warn(sb, _exception);
				}
			}
		}
		else {
			logger.warn("sendEmail() - Error sending email, session is null");
		}
		
		return returnVal;
	}
	
	public static boolean sendEmailNotification(final Class<?> _class, final String _method, final String _subject, final String _body) {
		NotificationContext context = new NotificationContext(1);
		
		try {
			context.setAttribute(NotificationContext.EMAIL_CONTEXT, EmailContext.getNotificationEmailContext(_class, _method, _subject, _body));
		}
		catch (ValidationException _exception) {
			logger.warn("Unable to generate EmailContext");
		}
		
		return EmailNotification.getInstance().send(context);
	}
	
	public static boolean sendEmailNotification(final Class<?> _class, final String _method, final String _subject, final String _body, final Exception _exception) {
		StringBuffer sb = new StringBuffer();
		
		// append current body if its not empty
		if(!Util.isStringEmpty(_body)) {
			sb.append(_body);
			
			// creating new lines after the original body and the exception stack trace
			sb.append("\n\n\n");
		}
		
		// append exception stack trace
		sb.append(getStackTrace(_exception));
		
		return sendEmailNotification(_class, _method, _subject, sb.toString());
	}
	
	/**
	 * Returns a comma delimeted string of the string passed.
	 * 
	 * @param String _string
	 * @return String
	 */
	public static String toCSV(final String[] _string) {
		StringBuffer sb = new StringBuffer();
		
		if(_string != null) {
			for(int i=0; i < _string.length; i++) {
				if(i > 0) {
					sb.append(',');
				}
				
				sb.append(_string[i]);
			}
		}
		
		return sb.toString();
	}
	
	/**
	 * Returns a String[] for the _string passed.
	 * 
	 * @param String _string
	 * @return String[]
	 */
	public static String[] toStringArray(final String _string, final String _delimiter) {
		StringTokenizer st = new StringTokenizer(_string, _delimiter);
		String[] stringArray = null;
		
		if(st.countTokens() > 0) {
			stringArray = new String[st.countTokens()];
		
			for(int i=0; st.hasMoreTokens(); i++) {
				stringArray[i] = trimString(st.nextToken());
			}
		}
		
		return stringArray;
	}
	
	/**
	 * Converts a integer string into an int, if the string is not a valid number
	 * than the default value is used.
	 * 
	 * @param String _integer
	 * @param int _defaultValue
	 * @return int
	 */
	public static int parseInt(final String _integer, final int _defaultValue) {
		int returnVal;
		
		try {
			returnVal = Integer.parseInt(Util.trimString(_integer));
		}
		catch(Exception _exception) {
			returnVal = _defaultValue;
		}
		
		return returnVal;
	}
	
	/**
	 * @param String _string
	 * @return boolean
	 */
	public static boolean containsSpecialCharacters(final String _string) {
		boolean returnVal = false;
		
		for(int i = 0; i < _string.length(); i++) {
			if(SPECIAL_CHARACTERS.indexOf(_string.charAt(i)) != -1) {
				returnVal = true;
				
				// break out of the loop;
				break;
			}
		}
		
		return returnVal;
	}
	
	/**
	 * Checks if the _searchString passed contains any of the token(s) specified.
	 * 
	 * @param String _tokens
	 * @param String _delimiter
	 * @param String _searchString
	 * @param boolean _exactMatch
	 * @return boolean
	 */
	public static boolean containsToken(final String _tokens, final String _delimiter, final String _searchString, final boolean _exactMatch) {
		StringTokenizer st = new StringTokenizer(_tokens, _delimiter);
		boolean returnVal = false;
		
		while(st.hasMoreTokens()) {
			if(_exactMatch) {
				if(_searchString.equals(st.nextToken())) {
					returnVal = true;
					st = null;
					break;
				}
			}
			else if(_searchString.indexOf(st.nextToken()) >= 0) {
				returnVal = true;
				st = null;
				break;
			}
		}
		
		return returnVal;
	}
	
	/**
	 * Checks the size of the String. If the String is null or if the String's size is
	 * greater than the allowed size, validation fails and false will be returned.
	 * 
	 * @param String _string
	 * @param int _maxLength
	 * @return boolean
	 */
	public static boolean isValidStringMinimumLength (final String _string, final int _minLength, final boolean _allowWhiteSpace) {
		boolean returnVal = true;
		
		if(_allowWhiteSpace) {
			if(_string == null || _string.trim().length() < _minLength) {
				returnVal = false;
			}
		}
		else {
			if(_string == null || _string.trim().equals("") || _string.trim().length() < _minLength) {
				returnVal = false;
			}
		}
		
		return returnVal;
	}
	
	/**
	 * Checks the size of the String. If the String is null or if the String's size is
	 * greater than the allowed size, validation fails and false will be returned.
	 * 
	 * @param String _string
	 * @param int _maxLength
	 * @return boolean
	 */
	public static boolean isValidStringMaximumLength (final String _string, final int _maxLength, final boolean _allowWhiteSpace) {
		boolean returnVal = true;
		
		if(_allowWhiteSpace) {
			if(_string == null || _string.trim().length() > _maxLength) {
				returnVal = false;
			}
		}
		else {
			if(_string == null || _string.trim().equals("") || _string.trim().length() > _maxLength) {
				returnVal = false;
			}
		}
		
		return returnVal;
	}
	
	/**
	 * Checks the size of the String. If the String is null or if the String's size is
	 * greater than the allowed size, validation fails and false will be returned.
	 * 
	 * @param String _string
	 * @param int _maxLength
	 * @return boolean
	 */
	public static boolean isValidStringLength (final String _string, final int _minLength, final int _maxLength, final boolean _allowWhiteSpace) {
		boolean returnVal = isValidStringMinimumLength(_string, _minLength, _allowWhiteSpace);
		
		if(returnVal) {
			returnVal = isValidStringMaximumLength(_string, _maxLength, _allowWhiteSpace);
		}
		
		return returnVal;
	}
	
	/**
	 * Validates phone number.
	 * 
	 * @param _phoneNumber
	 * @return
	 */
	public static boolean isValidPhoneNumber(final String _phoneNumber) {
		boolean returnVal = true;
		
		// TODO: Implement validation code
		returnVal = isValidStringLength(_phoneNumber, 10, 20, false);
		
		return returnVal;
	}
	
	/**
	 * Validates that its a valid email.
	 * 
	 * @param String _email
	 * @return
	 */
	public static boolean isValidEmail(final String _email) {
		Address emailAddress = null;
		boolean returnVal = isValidEmail(_email, emailAddress);
		emailAddress = null;
		
		return returnVal;
	}
	
	public static boolean isValidEmail(final String _email, Address _address) {
		boolean returnVal = true;
		
		try {
			_address = new InternetAddress(_email);
			returnVal = (_address != null);
		}
		catch(Exception _exception) {
			returnVal = false;
			_exception = null;
		}
		
		return returnVal;
	}
	
	/**
	 * Checks to see if the exception specified is a singleton instance
	 * created in the Framework's Constants.
	 * 
	 * @param Exception _exception
	 * @return boolean
	 */
	public static boolean isSingletonException(Exception _exception) {
		boolean returnVal = false;
		
		for(Field tempField : constantExceptionsfields) {
			try {
				if(tempField.get(null) == _exception) {
					returnVal = true;
					break;
				}
			}
			catch(Exception _exception1) {
				logger.error("error getting exceptions using reflections", _exception1);
			}
		}
		
		return returnVal;
	}
	
	public static void handleException(final Logger _logger, final String _message, Throwable _throwable) {
		if(_throwable instanceof Exception) {
			if(isSingletonException((Exception)_throwable)) {
				_logger.error(_message);
			}
			else {
				_logger.error(_message, _throwable);
				_throwable = null;
			}
		}
		else {
			_logger.error(_message, _throwable);
		}
	}
	
	/**
	 * Returns a string representation of the stack trace.
	 * 
	 * @param Throwable _throwable
	 * @return String
	 */
	public static String getStackTrace(final Throwable _throwable) {
		Writer result = new StringWriter();
		PrintWriter printWriter = new PrintWriter(result);
		
		_throwable.printStackTrace(printWriter);
		return result.toString();
	}
	
	/**
	 * This method will replace a key in the message with the specified value.
	 * This is useful when a message is something like:
	 * <code>This is a message for $numberOfUsers$</code>
	 * 
	 * <code>$numberOfUsers$</code> would be the key and the value would be
	 * whatever the caller specifies.
	 * 
	 * @param String _key
	 * @param String _value
	 */
	public static String replace(String _string, final String _key, final String _value) {
		String returnVal = _string;
		
		if(_string != null && _key != null && _value != null) {
			StringBuffer sb = new StringBuffer(_string);
			boolean containsMore = true;

			do {
				int indexOfKey = sb.indexOf(_key);

				if(indexOfKey < 0) {
					containsMore = false;
					returnVal = sb.toString();
				}
				else {
					sb.replace(indexOfKey, indexOfKey +_key.length(), _value);
				}
			} while(containsMore);
		}
		
		return returnVal;
	}
	
	public static Object lookup(Context _context, final String _jndi, boolean _close) {
		Object returnVal = null;
		
		if(_context == null) {
			_context = createContext();
			_close = true;
		}
		
		if(_context != null) {
			try {
				returnVal = _context.lookup(_jndi);
			}
			catch(Exception _exception) {
				logger.warn("Error looking up: " +_jndi);
			}
		}
		
		if(_close) {
			closeContext(_context);
		}
		
		return returnVal;
	}
	
	public static Context createContext() {
		Context returnVal = null;
		
		try {
			returnVal = new InitialContext();
		}
		catch(Exception _exception) {
			logger.warn("Error creating InitialContext", _exception);
		}
		
		return returnVal;
	}
	
	public static boolean closeContext(Context _context) {
		boolean returnVal = true;
		
		if(_context != null) {
			try {
				_context.close();
			}
			catch (NamingException _exception) {
				returnVal = false;
			}
		}
		
		return returnVal;
	}
}